/***
 * Web-Forker
 *
 * An abstract layer for Web-Workers to send and receive objects.
 *
 * References:
 *
 * 1. http://www.html5rocks.com/tutorials/workers/basics/
 *
 * 2. https://developer.mozilla.org/En/Using_web_workers
 *
 * Worker natively supported in Firefox, Chrome, Safari and Opera. 
 *
 * Except for IE, where used flash shim or setInterval...
 *
 * (Really flash shim actual only for IE<9)
 *
 * Copyright (c) 2012 Evgeny Burzak <buzzilo at gmail.moc>
 * Released under the MIT/X License
 */

// Notes
//
// Workers may access the navigator object, which is available within their scope.  
// It contains the following strings which can be used to identify the 
// browser, just as can be done from normal scripts:

//  appName
//  appVersion
//  platform
//  userAgent

if(typeof Forker == "undefined")
var Forker = (function(){

  // check for proprietary IE DOM members
  var is_ie = !!(typeof document !== "undefined" && document.uniqueID && document.expando)

  var _Forker = (function(){

    // Native Worker is available, just use it
    if(typeof Worker !== 'undefined') {
      return function(script){
        return new Worker(script);
      };
      
    // Emulate Web Workers on IE
    } else if(is_ie && _Forker.use_flash_shim) {
      return function(script){
        return fork_flash(script);
      };

    // One-threaded stub for others
    } else {
      return function(script){
        return fork_interval(script);
      };
    }

  })();

  if(is_ie) {
    
    // Flash shim disabled by default
    _Forker.use_flash_shim = false

    var lastForkerID = 0
    var width = 1
    var height = 1
    _Forker._forkers = []

  }

  function escapeArgString(str) {
    function replaceSpecialChar(match){
      if (match == "%")
        return "%25";
      else if (match == "&")
        return "%26";
      else if (match == "=")
        return "%3D";
      else if (match == "+")
        return "%2B";
    }
    return str.replace(/[%&=\+]/g, replaceSpecialChar);
  };

  // I will use quite tricky method to communicate with flash,
  // because of some chars cannot be transmitted (e.g. \x00) easily.
  function fork_flash(script_js) {

    var forkerID = ++lastForkerID

    var forker_swf = script_js.replace(/\.js$/, ".swf")

    var flashContainer = document.createElement('div')
    flashContainer.style.position = 'absolute'
    //flashContainer.style.overflow = "hidden"
    //flashContainer.style.width = flashContainer.style.height = "1px"
    flashContainer.style.right = flashContainer.style.top = "0px"
    var style = "position:absolute;right:0px;top:0px;"

    var initVars = "m="

    var flobject = ['<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"',
      '  width="',width,'" height="',height,'" id="__forker_',forkerID,'" style="',style,'">',
      '<param name="allowScriptAccess" value="always">',
      '<param name="movie" value="',forker_swf,'">',
      '<param name="quality" value="low">',
      '<param name="menu" value="false">',
      '<param name="wmode" value="transparent">',
      '<param name="FlashVars" value="',initVars,'">',
      '</object>', 
      "<script id=__fscmd_hlr_",forkerID," event=FSCommand(cmd,args) for=__forker_",forkerID,">",
          "if(Forker){",
             "Forker._forkers[",forkerID,"]._args=args;",
             "Forker._forkers[",forkerID,"]._fscmd(cmd);}","<\/script>"].join("");

    flashContainer.innerHTML = flobject 
    document.body.appendChild(flashContainer)

    var forker = {};
    
    forker._flashObject = flashContainer.firstChild
    forker._data = null
    forker._args = null
    forker._listeners = []
    forker.blobData = false
    forker.rawData = false

    var message = {};

    // I will reuse the code from fxcanvas project
    // http://code.google.com/p/fxcanvas/
    //
    forker._fscmd = function(type) {

      message.type = type

      if(this.blobData) {

        var data = []

        var mask, a, b, c, d;

        var len = Number(type)

        if(len) {

          for (var i=0; i<len; i++) {
            mask = this._args.charCodeAt(i),
            a = this._args.charCodeAt(i+1) ^ (mask & 0x2), 
            b = this._args.charCodeAt(i+2) ^ (mask & 0x4), 
            c = this._args.charCodeAt(i+3) ^ (mask & 0x8), 
            d = this._args.charCodeAt(i+4) ^ (mask & 0x10);

            data[i] = [a, b, c, d].join("")
          }

          message.data = data.join("")

        }
          
      } 

      else message.data = this._args


      // error
      if(type == "error") {
        if(this.onerror)
          this.onerror(message)
      }
      // message
      else {
        if(this.onmessage)
          this.onmessage(message)
      }

      for(var i=0; i<this._listeners.length; i++)
        if(this._listeners[i].type == type)
          this._listeners[i].fun(message)
    }

    forker.onmessage = function(msg){
    }

    //The error event has the following three fields that are of interest:

    //message
        //A human-readable error message.
    //filename
        //The name of the script file in which the error occurred.
    //lineno
        //The line number of the script file on which the error occurred. 

    forker.onerror = function(){
    }

    forker.terminate = function(){
      if(!this._flashObject) return // exit if forker was terminated
      flashContainer.removeChild(this._flashObject) // flash object
      flashContainer.removeChild(flashContainer.firstChild) // script
      document.body.removeChild(flashContainer)
      this._flashObject = null
    }

    var messageId = 0;

    forker.postMessage = function(msg){
      //trace("postMessage("+msg+")")
      // object serialization is not supported yet
      if( typeof msg === "object" ) throw Error("NOT_SUPPORTED_ERR");

      msg = ["b=", this.blobData ? "1" : "0", "&r=",this.rawData ? "1" : "0", "&i=",messageId++,
              "&m=", this.rawData ? msg : this.blobData ? this.blob2str(msg) : escapeArgString(msg)
            ].join("")
      this._flashObject.FlashVars = msg
    }

    forker.blob2str = function(blob){

      var m = [];

      // precalculate mask for bad chars
      //
      for (var i=0; i<256; i++) {
        switch (i) {
          case 0:  // \x00
          case 37: // %
          case 38: // &
          case 43: // +
          case 61: // =
              m[i] = 1;
              break;
          default:
              m[i] = 0;
        }
      }

      for(var i=0; i<imageData.data.length; i++) 
      {
        var pix = imageData.data[i],
            red = pix >> 24 & 0xFF,
            green = pix >> 16 & 0xFF,
            blue = pix >> 8 & 0xFF,
            alpha = pix & 0xFF,
            mask_red = m[red] << 1,
            mask_green = m[green] << 2,
            mask_blue = m[blue] << 3,
            mask_alpha = m[alpha] << 4;

            strBuf[strBuf.length] = String.fromCharCode( 
                            mask_red + mask_green + mask_blue + mask_alpha + 1,
                            red ^ mask_red, 
                            green ^ mask_green, 
                            blue ^ mask_blue, 
                            alpha ^ mask_alpha );
      }

      return blob
    }

    // type can be either 'message' or 'error'
    forker.addEventListener = function(type, fun, use_capture){
      if(use_capture == undefined)
        throw Error("use_capture is undefined")

      this._listeners.push({type: type, fun: fun})
    }

    _Forker._forkers[forkerID] = forker

    function terminateAll(){

      window.detachEvent("onbeforeunload", terminateAll);

      for(var i=0; i<_Forker._forkers.length; i++)
        if(_Forker._forkers[i])
          _Forker._forkers[i].terminate()
    }

    window.attachEvent("onbeforeunload", terminateAll);

    return forker

  };

  // Emulate Worker with setInterval 
  //
  function fork_interval(script_path) {

    var forker = {};
    var self = {};
    var message_queue = [];

    self.addEventListener = function(type, handler, use_capture){
      self.__listeners__.push({
        type: type,
        handler: handler
      })
    }

    self.postMessage = function(data){
      dispatchMessage(forker, "message", data)
    }
  
    function importScripts() {
      var args = Array.prototype.slice.call(arguments, 0)
      //trace("importScripts()", args)
      for(var i=0; i<args.length; i++) {
        getScript(args[i])
      }
    }

    self.importScripts = importScripts
    self.__listeners__ = [];
    self.navigator = window.navigator

    forker.__listeners__ = [];

    // get the scripts
    function getScript(script_path) 
    {
      //trace("getScript", script_path)
      var req = new XMLHttpRequest();
      req.open("GET", script_path, false)
      req.send(null);  
      if(req.status == 0 || req.status == 200) {
        var fun = Function("self", "importScripts", "window", "document", req.responseText)
        //trace("loaded", script_path)
        try{
          fun.call(self, self, importScripts, null, null);
        }catch(e){
          //trace("error", e.message)
          dispatchMessage(forker, "error", {
            message: e.message, 
            lineno: 0
          })
        }
      }
      else {
        var error_message = {
          message: "Error loading script",
          lineno: 0
        }
        dispatchMessage(forker, "error", error_message)
      }
    }

    function dispatchMessage(self, type, data){
      //trace("dispatchMessage()", type, data);

      var listeners = self.__listeners__, msg;

      msg = {
        type: type
      }
      if( type == "message" ) {
        msg.data = data 
      }
      else if( type == "error" ) {
        msg.message = data.message
        msg.lineno = data.lineno || 0
      }

      if( typeof self["on"+type] == "function" )
        self["on"+type](msg)

      for(var i=0; i<listeners.length; i++) {
        var listener = listeners[i]
        if( listener.type == type ) {
          
          if( listener.handler )
            listener.handler(msg)
        }
      }
    }

    // start up main loop
    var intId;

    intId = setInterval(function(){
      // load main script
      if(script_path) {
        importScripts(script_path)
        script_path = null
      }
      if(message_queue.length)
        try{
          dispatchMessage(self, "message", message_queue.shift())
        } catch(e){
          //trace(e.message)
          dispatchMessage(self, "error", e)
        }
    }, 100);

    forker.onmessage = function(msg){}

    forker.onerror = function(err){}

    forker.terminate = function(){
      forker.__listeners__ = null
      self.__listeners__ = null
      message_queue = null
      self = null
      forker = null
      clearInterval(intId)
    }

    forker.postMessage = function(msg){
      //trace("forker.postMessage("+msg+")")
      message_queue.push(msg)
    }

    forker.addEventListener = function(type, fun, use_capture){
      if(use_capture == undefined)
        throw Error("use_capture is undefined")

      forker.__listeners__.push({
        type: type,
        handler: handler
      })
    }
  
    return forker;

  };

  return _Forker;

})();
